package cn.sciento.starter.keyencrypt.json;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.google.common.base.Objects;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import cn.sciento.starter.keyencrypt.core.Encrypt;
import cn.sciento.starter.keyencrypt.core.EncryptContext;
import cn.sciento.starter.keyencrypt.core.EncryptType;
import cn.sciento.starter.keyencrypt.core.IEncryptionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;

public class EncryptedJsonSerializer extends JsonSerializer<Object> {
  private static final Logger logger = LoggerFactory.getLogger(EncryptedJsonSerializer.class);
  
  private final JsonSerializer<Object> baseSerializer;
  
  private final IEncryptionService encryptionService;
  
  private final Encrypt encrypt;
  
  private final JavaType javaType;
  
  public EncryptedJsonSerializer(IEncryptionService encryptionService, JsonSerializer<Object> baseSerializer, Encrypt encrypt, JavaType javaType) {
    this.baseSerializer = baseSerializer;
    this.encryptionService = encryptionService;
    this.encrypt = encrypt;
    this.javaType = javaType;
  }
  
  public void serialize(Object object, JsonGenerator generator, SerializerProvider provider) throws IOException {
    switch (EncryptContext.encryptType()) {
      case ENCRYPT:
      case TO_STRING:
        if (Arrays.stream(this.encrypt.ignoreValue()).noneMatch(item -> Objects.equal(item, String.valueOf(object)))) {
          serializeWithEncrypt(object, generator, provider, EncryptContext.encryptType());
          return;
        } 
        break;
    } 
    if (null == this.baseSerializer) {
      provider.defaultSerializeValue(object, generator);
    } else {
      this.baseSerializer.serialize(object, generator, provider);
    } 
  }
  
  private void serializeWithEncrypt(Object object, JsonGenerator generator, SerializerProvider provider, EncryptType encryptType) throws IOException {
    Object obj = object;
    Object currentObj = generator.getCurrentValue();
    if (obj instanceof Collection) {
      Class<?> elementType = (this.javaType instanceof com.fasterxml.jackson.databind.type.CollectionType) ? this.javaType.getContentType().getRawClass() : Object.class;
      if (BeanUtils.isSimpleValueType(elementType)) {
        writeSimpleCollection((Collection)obj, generator, provider, encryptType, currentObj);
      } else {
        logger.error("@Encrypt annotation is only allowed to add to simple attributes(including its array or collection), current : {} > {}", currentObj.getClass(), obj.getClass());
        writeUnsupported(obj, generator);
      } 
    } else if (obj.getClass().isArray()) {
      Class<?> elementType = obj.getClass().getComponentType();
      if (BeanUtils.isSimpleValueType(elementType)) {
        writeSimpleArray(obj, generator, provider, encryptType, currentObj);
      } else {
        logger.error("@Encrypt annotation is only allowed to add to simple attributes(including its array or collection), current : {} > {}", currentObj.getClass(), obj.getClass());
        writeUnsupported(obj, generator);
      } 
    } else if (BeanUtils.isSimpleValueType(obj.getClass())) {
      if (EncryptType.TO_STRING.equals(encryptType)) {
        generator.writeObject(String.valueOf(object));
      } else {
        generator.writeObject(innerEncrypt(object, generator, provider, currentObj));
      } 
    } else {
      logger.error("@Encrypt annotation is only allowed to add to simple attributes(including its array or collection), current : {} > {}", currentObj.getClass(), obj.getClass());
      writeUnsupported(obj, generator);
    } 
  }
  
  private void writeUnsupported(Object obj, JsonGenerator generator) throws IOException {
    generator.writeObject(obj);
  }
  
  private void writeSimpleCollection(Collection obj, JsonGenerator generator, SerializerProvider provider, EncryptType encryptType, Object currentObj) throws IOException {
    List<String> result = new ArrayList<>();
    for (Object child : obj) {
      if (EncryptType.TO_STRING.equals(encryptType)) {
        result.add(String.valueOf(child));
        continue;
      } 
      result.add(innerEncrypt(child, generator, provider, currentObj));
    } 
    generator.writeObject(result);
  }
  
  private void writeSimpleArray(Object obj, JsonGenerator generator, SerializerProvider provider, EncryptType encryptType, Object currentObj) throws IOException {
    List<String> result = new ArrayList<>();
    int length = Array.getLength(obj);
    for (int i = 0; i < length; i++) {
      if (EncryptType.TO_STRING.equals(encryptType)) {
        result.add(String.valueOf(Array.get(obj, i)));
      } else {
        result.add(innerEncrypt(Array.get(obj, i), generator, provider, currentObj));
      } 
    } 
    generator.writeObject(result);
  }
  
  protected String innerEncrypt(Object object, JsonGenerator generator, SerializerProvider provider, Object bean) throws IOException {
    StringWriter writer = new StringWriter();
    JsonGenerator nestedGenerator = generator.getCodec().getFactory().createGenerator(writer);
    if (null == this.baseSerializer) {
      provider.defaultSerializeValue(object, nestedGenerator);
    } else {
      this.baseSerializer.serialize(object, nestedGenerator, provider);
    } 
    nestedGenerator.close();
    String id = writer.getBuffer().toString().replace("\"", "");
    String encryptKey = this.encrypt.value();
    if (StringUtils.isNotBlank(this.encrypt.fieldName()))
      try {
        encryptKey = FieldUtils.readField(bean, this.encrypt.fieldName(), true).toString();
      } catch (IllegalAccessException illegalAccessException) {} 
    return this.encryptionService.encrypt(id, encryptKey);
  }
}
